// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

package logopt

import (
	"cmd/internal/obj"
	"cmd/internal/src"
	"encoding/json"
	"fmt"
	"internal/buildcfg"
	"io"
	"log"
	"net/url"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"unicode"
)

// 这将为Go编译器的-json选项实现（非）优化日志记录
// 该选项为-json 0，<destination>。
// 
// 0为版本号；为了避免同步更新的需要，如果出现新版本的日志记录，编译器将在一段时间内同时支持
// 和
// 两种版本，客户端将指定它们需要的内容。
// 
// <destination>是一个目录。
// 目录用前导/或os指定。路径分隔符，
// 或更明确地使用file:
// 目录路径（这通常是个坏主意，因为本地目录
// 在一个构建中变化很大，尤其是模块和/或供应商，并且可能
// 不可写）。
// 
// 对于编译的每个包，一个url。PathEscape（pkg）-命名的子目录
// 已创建。对于每个源文件。进入生成
// 诊断（没有诊断意味着没有文件）的包，
// 一个url。将创建PathEscape（文件）+“.json”-命名文件，其中包含
// 日志诊断。
// 
// 例如，“cmd%2Finternal%2Fdwarf/%3autogenerated%3E.json”
// 用于“cmd/internal/dwarf”和<autogenerated>（这不是一个真正的文件，但编译器看到了它）
// 
// 如果包字符串为空，则在内部用编码为%00的字符串（0）替换。
// 
// 每个日志文件都以标识版本、
// 平台和其他上下文的JSON记录开始，然后是优化相关的
// LSP诊断记录，每行一条（LSP版本3.15，这里使用的子集与3.14没有区别
// 参见https:
// 
// 诊断的字段按以下方式使用：
// 范围：最外面的源位置，目前开始和结束是相等的。
// 严重性：（始终）严重性信息（3）
// 来源：（始终）“go compiler”
// 代码：描述错过的优化的字符串，例如，“nilcheck”、“cannotInline”、“isInBounds”、“escape”
// 消息：根据代码，附加信息，例如，函数无法内联的原因。
// 相关信息：如果错过的优化实际上发生在范围内的函数上，
// 则内联位置的顺序出现在此处，从（第二）最外到最内，
// 每个位置都带有消息=“inlineoc”。
// 
// 在逃逸分析解释的情况下，在任何外部内联位置之后，
// 出现解释的行，如果逃逸流发生在内联函数中，则每个行后面都可能有自己的内联
// 位置。
// 
// 例如<destination>/cmd%2fcomile%2ffsa/prove。json 
// 可能以以下行开头（包装清晰）：
// 
// /{“版本”：0，“包”：“cmd/compile/internal/ssa”、“goos”：“darwin”、“goarch”：“amd64”，
// /“gc_版本”：“devel+e1b9a57852 Fri Nov 1 15:07:00 2019-0400”，
// 及更高版本包含（也为易读性而包装）：
// 
// /{“范围”：{“开始”：{“行”：191，“字符”：24}，“结束”：{“行”：191，“字符”：24}}，
// /“严重性”：3，“代码”：“nilcheck”，“源”：“go编译器”，“消息”：“，”
// /Users/drchase/work/go/src/cmd/compile/internal/ssa/func.go“，
// ”range:{“开始”：{“行”：153，“字符”：16}，“结束”：{“行”：153，“字符”：16}}}}，
// ”message:“inlineLoc”}
// 
// 也就是说，at prove.go（从上下文隐式，在文件名和标题行中提供），
// 第191行第24列，生成的代码中发生了nilcheck。
// 相关信息表明此代码实际上来自
// 对func的内联调用。开始，第153行，第16个字符。
// 
// 证明。围棋：191:
// ft.orderS=f.newPoset（）
// func。go:152和153:
// func（f*func）newPoset（）*poset{
// if len（f.Cache.scrpset）>0{
// 
// 如果包是空的，则在头记录中也会使用字符串（0）包名，例如
// 
// go tool compile-json=0，file:
// head-1 logopt/%00/x.json 
// /{“版本”：0，“包”：“\u0000”，“goos”：“darwin”，“goarch”：“amd64”，“gc_版本”：“devel+86487adf6a th11月7日19:34:2019-0500”，“文件”：“x.go”}

type VersionHeader struct {
	Version   int    `json:"version"`
	Package   string `json:"package"`
	Goos      string `json:"goos"`
	Goarch    string `json:"goarch"`
	GcVersion string `json:"gc_version"`
}

// 见https:

type DocumentURI string

type Position struct {
	Line      uint `json:"line"`      // gopls使用float64，但json输出与整数相同
	Character uint `json:"character"` // gopls使用float64，但json输出与整数相同
}

// 文本文档中表示为（从零开始）开始和结束位置的范围。
// 一个范围相当于编辑器中的一个选择。因此，结束位置是排他性的。
// 如果要指定一个包含行结束字符的行的范围
// 则使用结束位置表示下一行的开始。
type Range struct {
	/*Start defined:
	 * The range's start position
	 */
	Start Position `json:"start"`

	/*End defined:
	 * The range's end position
	 */
	End Position `json:"end"` // 独占
}

// 位置表示资源中的一个位置，例如文本文件中的一行。
type Location struct {
	// URI为
	URI DocumentURI `json:"uri"`

	// 范围为
	Range Range `json:"range"`
}

/* DiagnosticRelatedInformation defined:
 * Represents a related message and source code location for a diagnostic. This should be
 * used to point to code locations that cause or related to a diagnostics, e.g when duplicating
 * a symbol in a scope.
 */
type DiagnosticRelatedInformation struct {

	/*Location defined:
	 * The location of this related diagnostic information.
	 */
	Location Location `json:"location"`

	/*Message defined:
	 * The message of this related diagnostic information.
	 */
	Message string `json:"message"`
}

// 诊断Everity定义常量
type DiagnosticSeverity uint

const (
	/*SeverityInformation defined:
	 * Reports an information.
	 */
	SeverityInformation DiagnosticSeverity = 3
)

// 诊断AG定义常量
type DiagnosticTag uint

/*Diagnostic defined:
 * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects
 * are only valid in the scope of a resource.
 */
type Diagnostic struct {

	/*Range defined:
	 * The range at which the message applies
	 */
	Range Range `json:"range"`

	/*Severity defined:
	 * The diagnostic's severity. Can be omitted. If omitted it is up to the
	 * client to interpret diagnostics as error, warning, info or hint.
	 */
	Severity DiagnosticSeverity `json:"severity,omitempty"` // 始终为优化器日志记录提供严格的信息。

	/*Code defined:
	 * The diagnostic's code, which usually appear in the user interface.
	 */
	Code string `json:"code,omitempty"` // LSP使用'number | string'=gopls interface{}，但这里只使用字符串，例如“boundsCheck”、“nilcheck”等。

	/*Source defined:
	 * A human-readable string describing the source of this
	 * diagnostic, e.g. 'typescript' or 'super lint'. It usually
	 * appears in the user interface.
	 */
	Source string `json:"source,omitempty"` // “go compiler”

	/*Message defined:
	 * The diagnostic's message. It usually appears in the user interface
	 */
	Message string `json:"message"` // /有时使用，提供额外信息。

	/*Tags defined:
	 * Additional metadata about the diagnostic.
	 */
	Tags []DiagnosticTag `json:"tags,omitempty"` // 日志优化总是空的。

	/*RelatedInformation defined:
	 * An array of related diagnostic information, e.g. when symbol-names within
	 * a scope collide all definitions can be marked via this property.
	 */
	RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"`
}

// LoggedOpt是编译器产生和积累的数据，
// 将被转换为JSON供人类或IDE使用。
type LoggedOpt struct {
	pos          src.XPos      // 事件发生的源代码位置。如果它是内联的，那么外部和所有内联位置都将显示在JSON中。
	compilerPass string        // 编译器通过。供人/临时消费；未出现在JSON（尚未）
	functionName string        // 函数名中。供人/临时消费；没有出现在JSON（尚未）
	what         string        // 的（非）优化中；“nilcheck”、“boundsCheck”、“inline”、“noInline”
	target       []interface{} // 可选目标或参数“what”--什么是内联的、为什么不是内联的、副本大小等。第一个是最重要的/相关的。
}

type logFormat uint8

const (
	None  logFormat = iota
	Json0           // LSP 3.14和3.15版本0；LSP的未来版本可能会更改格式，在更新客户端时，编译器可能需要同时支持这两种格式。
)

var Format = None
var dest string

// LogJsonOption解析并验证附加到-json编译器标志的版本、目录值。
func LogJsonOption(flagValue string) {
	version, directory := parseLogFlag("json", flagValue)
	if version != 0 {
		log.Fatal("-json version must be 0")
	}
	dest = checkLogPath(directory)
	Format = Json0
}

// parseLogFlag检查传递给-json的标志
// 的版本、目标格式，并返回两部分。如果文件URI采用
func parseLogFlag(flag, value string) (version int, directory string) {
	if Format != None {
		log.Fatal("Cannot repeat -json flag")
	}
	commaAt := strings.Index(value, ",")
	if commaAt <= 0 {
		log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number", flag)
	}
	v, err := strconv.Atoi(value[:commaAt])
	if err != nil {
		log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number: err=%v", flag, err)
	}
	version = v
	directory = value[commaAt+1:]
	return
}

// Windows URI使用的格式，则返回true。网址。Parse包不专门处理Windows路径
// /（请参见golang/go#6027），因此我们检查URI路径是否有驱动器前缀（例如“/C:”）。
// （从tools/internal/span/uri.go复制）
// 这比文件路径中的处理更不全面。伊莎布斯在窗户上。
func isWindowsDriveURIPath(uri string) bool {
	if len(uri) < 4 {
		return false
	}
	return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
}

func parseLogPath(destination string) (string, string) {
	if filepath.IsAbs(destination) {
		return filepath.Clean(destination), ""
	}
	if strings.HasPrefix(destination, "file:// “{
		uri, err := url.Parse(destination)
		if err != nil {
			return "", fmt.Sprintf("optimizer logging destination looked like file:// URI但未能解析：err=%v”，err）
		}
		destination = uri.Host + uri.Path
		if isWindowsDriveURIPath(destination) {
			// strip leading/from/C:
			// /与tools/internal/span/URI.go不同，不要将驱动器号大写--让filepath.Clean执行它的操作。
			destination = destination[1:]
		}
		return filepath.Clean(destination), ""
	}
}

// checkLogPath对指定
// 优化器日志指向的目录的字符串进行初步检查，如果
// 通过测试，则将该字符串存储在LOU dir 
func checkLogPath(destination string) string {
	path, complaint := parseLogPath(destination)
	if complaint != "" {
		log.Fatalf(complaint)
	}
	err := os.MkdirAll(path, 0755)
	if err != nil {
		log.Fatalf("optimizer logging destination '<version>,<directory>' but could not create <directory>: err=%v", err)
	}
	return path
}

var loggedOpts []*LoggedOpt
var mu = sync.Mutex{} // mu保护loggedOpts中。

// NewLoggedOpt分配一个新的LoggedOpt，稍后作为“args”传递给NewLoggedOpt或LogOpt。
// Pos是源位置（包括内联），消息是什么，pass是哪个pass创建了消息，
// funcName是函数的名称
// /这是一个典型的用法，用于积累对错过的优化的解释，例如，为什么有东西逃逸了？
func NewLoggedOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt {
	pass = strings.Replace(pass, " ", "_", -1)
	return &LoggedOpt{pos, pass, funcName, what, args}
}

// Logopt记录有关编译器执行的优化（通常错过）的信息。
// Pos是源位置（包括内联），什么是消息，pass是哪个pass创建了消息，
// funcName是函数
func LogOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) {
	if Format == None {
		return
	}
	lo := NewLoggedOpt(pos, what, pass, funcName, args...)
	mu.Lock()
	defer mu.Unlock()
	// 的名称，因为来自后端的并发调用，不知道顺序是什么，但在使用前按外部Pos进行稳定排序。
	loggedOpts = append(loggedOpts, lo)
}

// Enabled返回是否启用了优化日志记录。zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zhi zh。
func Enabled() bool {
	switch Format {
	case None:
		return false
	case Json0:
		return true
	}
	panic("Unexpected optimizer-logging level")
}

type byPos struct {
	ctxt *obj.Link
	a    []*LoggedOpt
}

func (x byPos) Len() int { return len(x.a) }
func (x byPos) Less(i, j int) bool {
	return x.ctxt.OutermostPos(x.a[i].pos).Before(x.ctxt.OutermostPos(x.a[j].pos))
}
func (x byPos) Swap(i, j int) { x.a[i], x.a[j] = x.a[j], x.a[i] }

func writerForLSP(subdirpath, file string) io.WriteCloser {
	basename := file
	lastslash := strings.LastIndexAny(basename, "\\/")
	if lastslash != -1 {
		basename = basename[lastslash+1:]
	}
	lastdot := strings.LastIndex(basename, ".go")
	if lastdot != -1 {
		basename = basename[:lastdot]
	}
	basename = pathEscape(basename)

	// 假设一个目录，生成一个文件
	p := filepath.Join(subdirpath, basename+".json")
	w, err := os.Create(p)
	if err != nil {
		log.Fatalf("Could not create file %s for logging optimizer actions, %v", p, err)
	}
	return w
}

func fixSlash(f string) string {
	if os.PathSeparator == '/' {
		return f
	}
	return strings.Replace(f, string(os.PathSeparator), "/", -1)
}

func uriIfy(f string) DocumentURI {
	url := url.URL{
		Scheme: "file",
		Path:   fixSlash(f),
	}
	return DocumentURI(url.String())
}

// 返回文件名，用
// GOROOT的实际值替换第一次出现的$GOROOT（因为LSP不说“$GOROOT”）。
func uprootedPath(filename string) string {
	if !strings.HasPrefix(filename, "$GOROOT/") {
		return filename
	}
	return buildcfg.GOROOT + filename[len("$GOROOT"):]
}

// FlushLoggedOpts刷新所有累积的优化日志项。
func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
	if Format == None {
		return
	}

	sort.Stable(byPos{ctxt, loggedOpts}) // 稳定是保持每个函数顺序所必需的，它是可重复的。
	switch Format {

	case Json0: // LSP 3.15 
		var posTmp []src.Pos
		var encoder *json.Encoder
		var w io.WriteCloser

		if slashPkgPath == "" {
			slashPkgPath = "\000"
		}
		subdirpath := filepath.Join(dest, pathEscape(slashPkgPath))
		err := os.MkdirAll(subdirpath, 0755)
		if err != nil {
			log.Fatalf("Could not create directory %s for logging optimizer actions, %v", subdirpath, err)
		}
		diagnostic := Diagnostic{Source: "go compiler", Severity: SeverityInformation}

		// 对于LSP，为包和每个文件foo创建一个子目录。去，创造富。json在该子目录中。
		currentFile := ""
		for _, x := range loggedOpts {
			posTmp, p0 := x.parsePos(ctxt, posTmp)
			p0f := uprootedPath(p0.Filename())

			if currentFile != p0f {
				if w != nil {
					w.Close()
				}
				currentFile = p0f
				w = writerForLSP(subdirpath, currentFile)
				encoder = json.NewEncoder(w)
				encoder.Encode(VersionHeader{Version: 0, Package: slashPkgPath, Goos: buildcfg.GOOS, Goarch: buildcfg.GOARCH, GcVersion: buildcfg.Version, File: currentFile})
			}

			// 第一个“目标”是最重要的。
			var target string
			if len(x.target) > 0 {
				target = fmt.Sprint(x.target[0])
			}

			diagnostic.Code = x.what
			diagnostic.Message = target
			diagnostic.Range = newPointRange(p0)
			diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0]

			appendInlinedPos(posTmp, &diagnostic)

			// 内联信息后诊断解释存储在RelatedInformation中
			if len(x.target) > 1 {
				switch y := x.target[1].(type) {
				case []*LoggedOpt:
					for _, z := range y {
						posTmp, p0 := z.parsePos(ctxt, posTmp)
						loc := newLocation(p0)
						msg := z.what
						if len(z.target) > 0 {
							msg = msg + ": " + fmt.Sprint(z.target[0])
						}

						diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg})
						appendInlinedPos(posTmp, &diagnostic)
					}
				}
			}

			encoder.Encode(diagnostic)
		}
		if w != nil {
			w.Close()
		}
	}
}

// newPointRange为编译器源位置p返回一个位置范围。
func newPointRange(p src.Pos) Range {
	return Range{Start: Position{p.Line(), p.Col()},
		End: Position{p.Line(), p.Col()}}
}

// newLocation为编译器源位置p返回位置
func newLocation(p src.Pos) Location {
	loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newPointRange(p)}
	return loc
}

// appendInlinedPos从POSTP提取内联信息并将其附加到诊断
func appendInlinedPos(posTmp []src.Pos, diagnostic *Diagnostic) {
	for i := 1; i < len(posTmp); i++ {
		p := posTmp[i]
		loc := newLocation(p)
		diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"})
	}
}

func (x *LoggedOpt) parsePos(ctxt *obj.Link, posTmp []src.Pos) ([]src.Pos, src.Pos) {
	posTmp = ctxt.AllPos(x.pos, posTmp)
	// 反向邮戳将最外层放在第一位。
	l := len(posTmp)
	for i := 0; i < l/2; i++ {
		posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i]
	}
	p0 := posTmp[0]
	return posTmp, p0
}
